Pinvon's Blog

所见, 所闻, 所思, 所想

Accelerometer 计步 算法

概述

目前的计步方案:

  • 通过 GPS 获取运动距离, 反推步数.
  • 通过 Accelerometer 传感器计算步数.
  • Android4.4 以后, 使用 STEP COUNTER 和 STEP DETECTOR 两个传感器相结合.
  • 以上各种方案优势互补, 都用.

第一种方案在室外可行, 但如果用户在室内运动, 则不可行.

第二种方案比较通用, 算法如果设计得好, 可以比较精确.

第三种方案耗电较低, 也比较精确, 可行.

这边暂时只对第二种方案进一步讨论.

Accerometer(三轴加速器)

模型

x, y, z 轴代表的方向如图所示:

0.png

算法核心

计算 x, y, z 的矢量和, 这样可以平衡在某一个方向数值过大造成的数据误差, 然后将该值与上一时间点的值进行比较, 判断是否为波峰或波谷.

如果检测到了波峰, 并且符合时间差以及阈值的条件, 则判定为 1 步;

如果符合时间差条件, 波峰波谷差值大于初始值, 则将该差值纳入阈值的计算中.

所以, 检测是否为 1 步, 就是检测符合条件的波峰. 条件有如下三个:

  • 曲线连续上升的次数
  • 波峰波谷的差值大于阈值
  • 阈值是动态改变的

初始值

算法的开始, 要先设置一些初始值. 如:

  • 动态阈值: initialValue = 1.3
  • 初始阈值: threadValue = 2.0
  • 波峰波谷时间差: timeInterval = 250

计算矢量和

\(gravity = (x^2 + y^2 + z^2)^{\frac{1}{2}}\)

检测

如果 gravity == 0, 说明是第一次检测, 将值赋给 gravityOld;

如果 gravity != 0, 就将当前值与 gravityOld 比较;

记录上一次的状态 lastStatus 是上升还是下降.

如果当前值比 gravityOld 大, 说明在上升, 更新是否上升的标志 isDirectionUp, 更新持续上升次数 continueUpCount.

如果当前值比 gravityOld 小, 说明在下降. 需要将持续上升次数保存到变量 continueUpFormerCount, 然后将 continueUpCount 置 0, isDirectionUp 置为 false.

判断是否为波峰, 主要是判断这几项: 不再上升(isDirectionUp), 上一次的状态是上升(lastStatus), 上一次上升的次数大于2(continueUpFormerCount), 上一次的矢量和大于20(gravityOld).

如果是波峰, 进一步判断时间差是否符合条件, 波峰波谷的差值是否符合条件. 如果符合条件, 则步数加 1.

如果时间差符合条件, 而波峰波谷的差值大于阈值, 则将该差值纳入阈值的计算中.

import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  View
} from 'react-native';
import { Accelerometer } from "react-native-sensors";

const Value = ({name, value}) => (
  <View style={styles.valueContainer}>
    <Text style={styles.valueName}>{name}:</Text>
    <Text style={styles.valueValue}>{new String(value).substr(0, 8)}</Text>
  </View>
)

class Step extends Component {
  constructor(props) {
    super(props);

    new Accelerometer({
      updateInterval: 400 // defaults to 100ms
    })
      .then(observable => {
        observable.subscribe(({x,y,z}) => this.setState({x,y,z}));
      })
      .catch(error => {
        console.log("The sensor is not available");
      });

    this.state = {x: 0, y: 0, z: 0};
    this.gravityOld             = 0;        // 上一次的矢量和
    this.lastStatus             = false;    // 上一次的状态, 上升还是下降
    this.isDirectionUp          = false;    // 是否继续上升
    this.continueUpCount        = 0;        // 持续上升次数
    this.continueUpFormerCount  = 0;        // 上一次持续上升的次数
    this.peakOfWave             = 0;        // 波峰值
    this.valleyOfWave           = 0;        // 波谷值
    this.timeOfThisPeak         = 0;        // 到达波峰花费的时间
    this.timeOfLastPeak         = 0;        // 上次到达波峰花费的时间
    this.timeOfNow              = 0;        // 当前时间
    this.timeInterval           = 250;      // 波峰波谷时间差
    this.initialValue           = 1.3;      // 用于计算动态阈值
    this.threadValue            = 2.0;      // 初始阈值
    this.steps                  = 0;        // 当前步数
    this.tempValue              = [];       // 存放波峰波谷差值
    this.tempCount              = 0;
  }

  _getSteps = () => {
      this._detectNewStep(this._average());
      return this.steps;
  }

  // 计算 x y z 的平均值
  _average = () => Math.sqrt(this.state.x * this.state.x + this.state.y * this.state.y + this.state.z * this.state.z)

  _detectNewStep = ( gravity ) => {
    if (this.gravityOld === 0) {
        this.gravityOld = gravity;
    } else {
        if (this._detectPeak(gravity, this.gravityOld)) {
            this.timeOfLastPeak = this.timeOfThisPeak;
            this.timeOfNow = new Date().getTime();
            console.log('threshold', this.peakOfWave-this.valleyOfWave);
            if ((this.timeOfNow - this.timeOfLastPeak >= this.timeInterval) && (this.peakOfWave - this.valleyOfWave >= this.threadValue)) {
                this.timeOfThisPeak = this.timeOfNow;
                this.steps = this.steps + 1;
                console.log('pinvon step', this.steps);
            }
            if ((this.timeOfNow - this.timeOfLastPeak >= this.timeInterval) && (this.peakOfWave - this.valleyOfWave >= this.initialValue)) {
                this.timeOfThisPeak = this.timeOfNow;
                this.threadValue = this._peakValleyThread(this.peakOfWave - this.valleyOfWave);
                console.log('pinvon', this.threadValue);
            }
        }
    }
    this.gravityOld = gravity;
  }

  _peakValleyThread = ( value ) => {
      console.log('pinvon', '_peakValleyThread', value);
      var tempThread = this.threadValue;
      if (this.tempCount < 4) {
          this.tempValue[this.tempCount] = value;
          this.tempCount = this.tempCount + 1;
      } else {
          this.tempThread = this._averageValue(this.tempValue, 4);
          for (var i = 1; i < 4; i++) {
              this.tempValue[i-1] = this.tempValue;
          }
          this.tempValue[3] = value;
      }
      return tempThread;
  }

  _averageValue = (value, n) => {
      var ave = 0;
      for (var index = 0; index < n; index++) {
          ave = ave + value[index];
      }
      ave = ave / 4;
      if (ave > 0) {
          ave = 4.3;
      } else if (ave >= 7 && ave < 8) {
          ave = 3.3;
      } else if (ave >= 4 && ave < 7) {
          ave = 2.3;
      } else if (ave >= 3 && ave < 4) {
          ave = 2.0;
      } else {
          ave = 1.3;
      }
      return ave;
  }

  _detectPeak = (newValue, oldValue) => {
      this.lastStatus = this.isDirectionUp;
      if (newValue >= oldValue) {
          this.isDirectionUp = true;
          this.continueUpCount = this.continueUpCount + 1;
      } else {
          this.continueUpFormerCount = this.continueUpCount;
          this.continueUpCount = 0;
          this.isDirectionUp = false;
      }

      if (!this.isDirectionUp && this.lastStatus && (this.continueUpFormerCount >= 2 || oldValue >= 20)) {
          this.peakOfWave = oldValue;
          return true;
      } else if (!this.lastStatus && this.isDirectionUp) {
          this.valleyOfWave = oldValue;
          return false;
      } else {
          return false;
      }
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.headline}>
          Accelerometer values
        </Text>
        <Value name="x" value={this.state.x} />
        <Value name="y" value={this.state.y} />
        <Value name="z" value={this.state.z} />
        <Value name="step" value={this._getSteps()} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  headline: {
    fontSize: 30,
    textAlign: 'center',
    margin: 10,
  },
  valueContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
  },
  valueValue: {
    width: 200,
    fontSize: 20
  },
  valueName: {
    width: 50,
    fontSize: 20,
    fontWeight: 'bold'
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

export default Step;

Comments

使用 Disqus 评论
comments powered by Disqus